/*
    Copyright © 2010  Ahti Legonkov
    
    This file is part of j+-.
    
    j+- is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    j+- is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.
    
    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package cx.ath.skyflyer.javapm.symbol;

import japa.parser.ast.ImportDeclaration;
import japa.parser.ast.Node;
import japa.parser.ast.PackageDeclaration;
import japa.parser.ast.body.ClassOrInterfaceDeclaration;
import japa.parser.ast.body.EnumConstantDeclaration;
import japa.parser.ast.body.EnumDeclaration;
import japa.parser.ast.body.FieldDeclaration;
import japa.parser.ast.body.MethodDeclaration;
import japa.parser.ast.body.Parameter;
import japa.parser.ast.body.TypeDeclaration;
import japa.parser.ast.expr.VariableDeclarationExpr;
import japa.parser.ast.type.Type;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map.Entry;

/**
 * Basically this is a singly linked list of symbol tables. For each scope in
 * program there is a list of nested scopes that contain symbols visible in
 * that scope.
 * @author Ahti Legonkov
 */
public class SymbolTable {

    /**
     * Maps name to it's declarations. There may be many declarations due to
     * overloading - several methods may have same name, and class field and
     * a method may also share common name.
     */
    private Hashtable<String, List<Node>> table;

    /** Reference to a symbol table of containing scope. */
    private final SymbolTable prev;

    /** Type of scope. Mostly for debugging. */
    private final ScopeType type;

    /**
     * Creates a new Symbol Table for global scope. There should be exactly
     * one of these around in the symbol table tree.
     */
    public SymbolTable() {
        this(null, ScopeType.Global);
    }

    /**
     * Creates a new Symbol Table.
     * @param prev a reference to containing scope.
     * @param type type of scope.
     */
    public SymbolTable(SymbolTable prev, ScopeType type) {
        table = new Hashtable();
        this.prev = prev;
        this.type = type;
    }

    /**
     * Returns an iterator to symbols in the scope. This iterator will not
     * iterate over all symbols that are visible in the scope, only the ones
     * that are in this scope.
     * This method is meant for debugging purposes only.
     */
    public Iterator symbols() {
        return table.keySet().iterator();
    }

    /**
     * Returns symbol table for global scope.
     */
    public SymbolTable global() {
        SymbolTable e = this;
        while (e.prev != null) {
            e = e.prev;
        }
        return e;
    }

    /**
     * Add a symbol to the table.
     * @param name name used to lookup the symbol.
     * @param i symbol declaration in the AST.
     */
    public void put(String name, Node n) {
        assert(name != null);
        assert(n != null);
        
        List<Node> nodes = table.get(name);
        if (nodes == null) {
            nodes = new LinkedList();
            table.put(name, nodes);
        }
        nodes.add(n);
    }

    /**
     * Replaces existing mapping name => current with name => replacement.
     * @param name
     * @param current
     * @param replacement
     * @return true if mapping is replaced.
     */
    public boolean replace(String name, Node current, Node replacement) {
        assert(name != null);
        assert(current != null);
        assert(replacement != null);
        
        for (SymbolTable e = this; e != null; e = e.prev) {
            List<Node> found = e.table.get(name);
            if (found == null)
                continue;

            if (found.remove(current)) {
                found.add(replacement);
                return true;
            }
        }
        return false;
    }

    /**
     * Get list of declarations for given name.
     * @param name
     * @return null if failed.
     */
    public List<Node> getList(String name) {
        return getList(name, Integer.MAX_VALUE);
    }

    /**
     * Get list of declarations for given name. Search can be limited by depth.
     * @param name
     * @param maxSearchDepth
     * @return null if failed.
     */
    private List<Node> getList(String name, int maxSearchDepth) {
        assert(name != null);
        assert(maxSearchDepth > 0);
    
        for (SymbolTable e = this; maxSearchDepth > 0 && e != null; e = e.prev, --maxSearchDepth) {
            List<Node> found = e.table.get(name);
            if (found != null)
                return found;
        }
    
        return null;
    }

    /**
     * Gets node for anything but method. The reason for this is that methods can be overloaded.
     * @param name symbol name.
     * @return null if not found or symbol is a method.
     */
    public Node getNode(String name) {
        for (SymbolTable e = this; e != null; e = e.prev) {
            List<Node> found = e.table.get(name);
            if (found == null)
                continue;

            final Class methodClass = MethodDeclaration.class;

            for (Node n : found) {
                assert(n != null);
                if (!methodClass.isAssignableFrom(n.getClass()))
                    return n;
            }

            return null;
        }

        return null;
    }

    private <T extends Node> T getNode(String name, Class<T> c) {
        for (SymbolTable e = this; e != null; e = e.prev) {
            List<Node> found = e.table.get(name);
            if (found == null)
                continue;

            for (Node n : found) {
                assert(n != null);
                try {
                    //if (n.getClass().equals(c)) {
                    if (c.isAssignableFrom(n.getClass()))
                        return (T)n;
                }
                catch (ClassCastException ex) {
                    // Why can't I use instanceof with generic
                    // arguments?
                    //System.err.println(ex.toString());
                }
            }
            
            return null;
        }

        return null;
    }

    /**
     * Finds a type with specific name, be it class, interface or enumeration.
     * @param name
     * @return
     */
    public TypeDeclaration getType(String name) {
        return getNode(name, TypeDeclaration.class);
    }

    public ImportDeclaration getImport(String name) {
        return getNode(name, ImportDeclaration.class);
    }

    /**
     * Finds class with specific name.
     * @param name
     * @return a list of all class or interface declarations that match the
     * name. An empty list is returned if no declarations are found.
     */
    public ClassOrInterfaceDeclaration getClass(String name) {
        return getNode(name, ClassOrInterfaceDeclaration.class);
    }

    /**
     * Finds enumeration with specific name.
     * @param name
     * @return
     */
    public EnumDeclaration getEnum(String name) {
        return getNode(name, EnumDeclaration.class);
    }

    /**
     *
     * @param name
     * @param argumentTypes
     * @return
     */
    public MethodDeclaration getMethod(String name, List<Type> argumentTypes) {
        assert(argumentTypes != null);
        assert(name != null);

        final int argumentCount = argumentTypes.size();

        for (SymbolTable e = this; e != null; e = e.prev) {
            final List<Node> found = e.table.get(name);
            if (found == null)
                continue;

            for (Node symbol : found) {
                if (!(symbol instanceof MethodDeclaration))
                    continue;

                final MethodDeclaration m = (MethodDeclaration)symbol;
                if (m.getParameters() == null && argumentCount != 0)
                    continue;

                if (argumentCount > 0 && m.getParameters().size() != argumentCount)
                    continue;

                boolean argumentListsMatch = true;
                for (int i = 0; i < argumentCount; ++i) {
                    final Type requiredArgType = argumentTypes.get(i);
                    final Type methodArgType = m.getParameters().get(i).getType();
                    if (!requiredArgType.equals(methodArgType)) {
                        argumentListsMatch = false;
                        break;
                    }
                }

                if (argumentListsMatch)
                    return m;
            }
        }

        return null;
    }

    /**
     * Gets all method declarations that declare a method with given name and
     * have specific number of arguments.
     * @param name - name of methods to find
     * @param argumentCount - number of arguments that the methods must have.
     * @return list of method declarations. An empty list means that nothing
     * was found. This method does not return null.
     */
    public List<MethodDeclaration> getMethod(String name, int argumentCount) {

        List<MethodDeclaration> res = new LinkedList();

        for (SymbolTable e = this; e != null; e = e.prev) {
            List<Node> found = e.table.get(name);
            if (found == null)
                continue;

            for (Node symbol : found) {
                if (!(symbol instanceof MethodDeclaration))
                    continue;

                MethodDeclaration m = (MethodDeclaration)symbol;
                if (m.getParameters() == null && argumentCount != 0)
                    continue;

                if (argumentCount > 0 && m.getParameters().size() != argumentCount)
                    continue;

                res.add(m);
            }
        }

        return res;
    }

    private ClassOrInterfaceDeclaration getCurrentClass() {
        for (SymbolTable st = this; st != null; st = st.prev) {
            for (Entry<String,List<Node>> entry : st.table.entrySet()) {
                for (Node node : entry.getValue()) {
                    if (node instanceof ClassOrInterfaceDeclaration)
                        return (ClassOrInterfaceDeclaration)node;
                }
            }
        }

        return null;
    }

    /**
     * Gets a package declaration with specific name. If package is not found
     * the method returns null.
     * @param name name of package to find.
     * @return reference to package declaration.
     */
    public PackageDeclaration getPackage(String name) {
        return getNode(name, PackageDeclaration.class);
    }

    public FieldDeclaration getField(String name) {
        return getNode(name, FieldDeclaration.class);
    }

    public EnumConstantDeclaration getEnumConstant(String name) {
        return getNode(name, EnumConstantDeclaration.class);
    }

    public VariableDeclarationExpr getVariable(String name) {
        return getNode(name, VariableDeclarationExpr.class);
    }

    public Parameter getParameter(String name) {
        return getNode(name, Parameter.class);
    }
}
